﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace Gunningine
{
    [StructLayout(LayoutKind.Sequential)]
    public struct Vector3
    {
        public float X;

        public float Y;

        public float Z;

        /// <summary>
        /// Length of vector.
        /// </summary>
        public float Length
        {
            get
            {
                return (float)Math.Sqrt(this.SquaredLength);
            }
        }

        /// <summary>
        /// Squared length, useful for when you just need to compare two lengths.
        /// </summary>
        public float SquaredLength
        {
            get
            {
                return this.X * this.X + this.Y * this.Y + this.Z * this.Z;
            }
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="x">x value</param>
        /// <param name="y">y value</param>
        /// <param name="z">z value</param>
        public Vector3(float x, float y, float z)
        {
            this.X = x;
            this.Y = y;
            this.Z = z;
        }

        /// <summary>
        /// Copy constructor.
        /// </summary>
        /// <param name="that">other vector.</param>
        public Vector3(Vector3 that)
        {
            this.X = that.X;
            this.Y = that.Y;
            this.Z = that.Z;
        }

        /// <summary>
        /// Gets or sets the value at the index of the Vector.
        /// </summary>
        public float this[int index]
        {
            get
            {
                if (index == 0) return this.X;
                else if (index == 1) return this.Y;
                else if (index == 2) return this.Z;
                throw new IndexOutOfRangeException("You tried to access this vector at index: " + index);
            }
            set
            {
                if (index == 0) this.X = value;
                else if (index == 1) this.Y = value;
                else if (index == 2) this.Z = value;
                else throw new IndexOutOfRangeException("You tried to set this vector at index: " + index);
            }
        }

        /// <summary>
        /// Unary minus operator.
        /// </summary>
        /// <param name="a">vector.</param>
        /// <returns>vector.</returns>
        public static Vector3 operator -(Vector3 a)
        {
            return new Vector3(-a.X, -a.Y, -a.Z);
        }

        /// <summary>
        /// Addition operator.
        /// </summary>
        /// <param name="a">vector.</param>
        /// <param name="b">vector.</param>
        /// <returns>vector.</returns>
        public static Vector3 operator +(Vector3 a, Vector3 b)
        {
            return new Vector3(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
        }

        /// <summary>
        /// Subtraction operator.
        /// </summary>
        /// <param name="a">vector.</param>
        /// <param name="b">vector.</param>
        /// <returns>vector.</returns>
        public static Vector3 operator -(Vector3 a, Vector3 b)
        {
            return new Vector3(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
        }

        /// <summary>
        /// Multiplication operator.
        /// </summary>
        /// <param name="a">vector.</param>
        /// <param name="b">scalar.</param>
        /// <returns>vector.</returns>
        public static Vector3 operator *(Vector3 a, float scalar)
        {
            return new Vector3(a.X * scalar, a.Y * scalar, a.Z * scalar);
        }

        /// <summary>
        /// Division operator.
        /// </summary>
        /// <param name="a">vector.</param>
        /// <param name="b">scalar.</param>
        /// <returns>vector.</returns>
        public static Vector3 operator /(Vector3 a, float scalar)
        {
            float recip = 1.0f / scalar;
            return new Vector3(a.X * recip, a.Y * recip, a.Z * recip);
        }

        /// <summary>
        /// Normalise this vector.
        /// </summary>
        public void Normalise()
        {
            double len = Math.Sqrt(this.X * this.X + this.Y * this.Y + this.Z * this.Z);
            if (len > 1e-08)
            {
                float invLen = (float)(1.0 / len);
                this.X *= invLen;
                this.Y *= invLen;
                this.Z *= invLen;
            }
        }

        /// <summary>
        /// Normalise a copy of this vector and return it.
        /// </summary>
        /// <returns>a copy of this vector, normalised.</returns>
        public Vector3 Normalised()
        {
            Vector3 vec = new Vector3(this.X, this.Y, this.Z);
            vec.Normalise();
            return vec;
        }

        /// <summary>
        /// Cross-product of two vectors
        /// </summary>
        /// <param name="a">vector.</param>
        /// <param name="b">scalar.</param>
        /// <returns>vector.</returns>
        public static Vector3 CrossProduct(Vector3 a, Vector3 b)
        {
            Vector3 result = new Vector3(a.Y * b.Z - a.Z * b.Y,
                                            a.Z * b.X - a.X * b.Z,
                                            a.X * b.Y - a.Y * b.X);
                
            return result;
        }

        /// <summary>
        /// Dot-product of two vectors.
        /// </summary>
        /// <param name="a">first vector.</param>
        /// <param name="b">second vector.</param>
        /// <returns>dot product.</returns>
        public static float DotProduct(Vector3 a, Vector3 b)
        {
            return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
        }

        /// <summary>
        /// Calculate the angle between two (normalised!) vectors.
        /// </summary>
        /// <param name="a">first vector.</param>
        /// <param name="b">second vector.</param>
        /// <returns>angle between.</returns>
        public static float AngleBetween(Vector3 a, Vector3 b)
        {
            return MathsUtils.RadToDeg(Math.Acos(Vector3.DotProduct(a, b)));
        }

        /// <summary>
        /// Rotate a vector (point) around a given axis.
        /// </summary>
        /// <param name="vec">point to rotate.</param>
        /// <param name="angle">angle to rotate by.</param>
        /// <param name="axis">axis to rotate around.</param>
        /// <returns></returns>
        public static Vector3 Rotate(Vector3 vec, float angle, Vector3 axis)
        {
            float newX = 0, newY = 0, newZ = 0;

            float cosAngle = (float)Math.Cos(MathsUtils.DegToRad(angle));
            float sinAngle = (float)Math.Sin(MathsUtils.DegToRad(angle));

            newX += (cosAngle + (1 - cosAngle) * axis.X * axis.X) * vec.X;
            newX += ((1 - cosAngle) * axis.X * axis.Y - axis.Z * sinAngle) * vec.Y;
            newX += ((1 - cosAngle) * axis.X * axis.Z + axis.Y * sinAngle) * vec.Z;

            newY += ((1 - cosAngle) * axis.X * axis.Y + axis.Z * sinAngle) * vec.X;
            newY += (cosAngle + (1 - cosAngle) * axis.Y * axis.Y) * vec.Y;
            newY += ((1 - cosAngle) * axis.Y * axis.Z - axis.X * sinAngle) * vec.Z;

            newZ += ((1 - cosAngle) * axis.X * axis.Z - axis.Y * sinAngle) * vec.X;
            newZ += ((1 - cosAngle) * axis.Y * axis.Z + axis.X * sinAngle) * vec.Y;
            newZ += (cosAngle + (1 - cosAngle) * axis.Z * axis.Z) * vec.Z;

            return new Vector3(newX, newY, newZ);
        }

        /// <summary>
        /// Absolute distance between two vectors.
        /// </summary>
        /// <param name="vec">vector.</param>
        /// <returns>distance between them.</returns>
        public float Distance(Vector3 vec)
        {
            return (this - vec).Length;
        }

        /// <summary>
        /// Interpolate to target position
        /// </summary>
        /// <param name="to">target position</param>
        /// <param name="distance">distance in [0, 1].</param>
        /// <returns></returns>
        public Vector3 Lerp(Vector3 to, float distance)
        {
            return new Vector3(this.X + (to.X - this.X) * distance, 
                               this.Y + (to.Y - this.Y) * distance,
                               this.Z + (to.Z - this.Z) * distance);
        }

        /// <summary>
        /// Absolute (squared) distance between two vectors.
        /// Useful for example, when you want to find the closest
        /// point to another, but don't care about the actual distance.
        /// </summary>
        /// <param name="vec">vector.</param>
        /// <returns>distance between them.</returns>
        public float SquaredDistance(Vector3 vec)
        {
            return (this - vec).SquaredLength;
        }

        /// <summary>
        /// GetHashCode.
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            return this.X.GetHashCode() ^ this.Y.GetHashCode() ^ this.Z.GetHashCode();
        }

        public override string ToString()
        {
            return String.Format("({0:F2}, {1:F2}, {2:F2})", this.X, this.Y, this.Z);
        }


        public static readonly Vector3 UnitX = new Vector3(1, 0, 0);
            
        public static readonly Vector3 UnitY = new Vector3(0, 1, 0);

        public static readonly Vector3 UnitZ = new Vector3(0, 0, 1);

        public static readonly Vector3 NegativeUnitX = new Vector3(-1, 0, 0);

        public static readonly Vector3 NegativeUnitY = new Vector3(0, -1, 0);

        public static readonly Vector3 NegativeUnitZ = new Vector3(0, 0, -1);

        public static readonly Vector3 Zero = new Vector3(0, 0, 0);

        public static readonly Vector3 One = new Vector3(1, 1, 1);
    }
}
